﻿Imports Microsoft.Xna.Framework
Imports Microsoft.Xna.Framework.Graphics

''' <summary>
''' This class takes a heightmap image, and list of colors. It will auto-
''' generate all the vertices and color them in slices.
''' </summary>
''' <remarks>
''' A lot of this class is loosly based on Riemers XNA Tutorial 1 from
''' http://www.riemers.net/eng/Tutorials/XNA/Csharp/series1.php
''' Effects file 'effects.fx' is downloaded from the same location.
''' 
''' Currently this class is doing two jobs at once, each of which should
''' be split out separately.
''' 
''' 
''' Job 1.) Take a heightmap and create arrays of vertices and indexes
'''        and apply colors.
''' 
''' Job 2.) Draw the model.
''' 
''' 
''' Planned TODO:
''' 
''' 1.)    Make it create a model from the calculated vertices. This will 
'''        save the job of calculating normals.
''' 
''' 2.)    Make it create an object of HeightMapInfo to contain and expose
'''        relevant data.
''' 
''' 3.)    Accept more user preferences, e.g. scale.
''' 
''' 4.)    Take the parts of this class that create the model and make a
'''        custom content processor that handles this job.
'''
''' 5.)    Investigate why directional lighting does not seem to work and
'''        fix.
''' 
''' 6.)    Eventually, make it able to accept either textures or colors.
''' 
''' </remarks>
Public Class AutoTerrain
    Inherits GameObject
    Implements ITerrain

#Region " Fields "
    Private mVertices As VertexPositionNormalColor()
    Private mEffect As Effect
    Private mVertexDeclaration As VertexDeclaration
    Private mIndices As Integer()
    Private mTerrainWidth As Integer = 4
    Private mTerrainHeight As Integer = 3
    Private mHeightData As Single(,)
    Private mHeightMap As Texture2D
    Private mTerrainBumpiness As Single = 5
    Private mColors As New List(Of Color)
    Private mLightDirection As Vector3
    Private mAmbientLightingLevel As Single = 0.1
    Private mVertexBuffer As VertexBuffer
    Private mIndexBuffer As IndexBuffer
    Private mHeightMapInfo As HeightMapInfo
#End Region ' Fields

#Region " Initialization "
    Public Sub New(ByVal GameManager As GameManager, Optional ByVal terrainBumpiness As Single = 5)
        MyBase.New(GameManager)
        mTerrainBumpiness = MathHelper.Clamp(terrainBumpiness, 0.5, 50)
        LightDirection = New Vector3(1.0F, -1.0F, -1.0F)
    End Sub

    Public Sub New(ByVal GameManager As GameManager, ByVal position As Vector3, Optional ByVal terrainBumpiness As Single = 5)
        MyClass.New(GameManager, terrainBumpiness)
        Me.Position = Position
    End Sub

    Public Overrides Sub LoadContent() Implements ITerrain.LoadContent
        MyBase.LoadContent()

        mEffect = GameManager.Game.Content.Load(Of Effect)("effects")

        LoadHeightData(mHeightMap)
        SetUpVertices()
        SetUpIndices()

        CalculateNormals()
        CopyToBuffers()
        Offset = New Vector3(TerrainWidth / -2, 0, TerrainHeight / 2)

        mHeightMapInfo = New HeightMapInfo(mHeightData, 1)
    End Sub

    Public Overrides Sub UnloadContent() Implements ITerrain.UnloadContent
        MyBase.UnloadContent()
    End Sub

    Public Overrides Sub Initialize() Implements ITerrain.Initialize
        MyBase.Initialize()
    End Sub
#End Region ' Initialization

#Region " Properties "
    Public Property LightDirection() As Vector3
        Get
            LightDirection = mLightDirection
        End Get
        Set(ByVal value As Vector3)
            mLightDirection = value
            mLightDirection.Normalize()
        End Set
    End Property

    ''' <value>Between 0 and 1</value>
    Public Property AmbientLightingLevel() As Single
        Get
            AmbientLightingLevel = mAmbientLightingLevel
        End Get
        Set(ByVal value As Single)
            mAmbientLightingLevel = MathHelper.Clamp(value, 0, 1)
        End Set
    End Property

    ''' <summary>
    ''' Width of the terrain (created from the heightmap). 
    ''' i.e. size in the X direction.
    ''' </summary>
    Public ReadOnly Property TerrainWidth() As Integer
        Get
            TerrainWidth = mTerrainWidth
        End Get
    End Property

    ''' <summary>
    ''' Length of the terrain (created from the heightmap).
    ''' i.e. size in the Z direction.
    ''' </summary>
    Public ReadOnly Property TerrainHeight() As Integer
        Get
            TerrainHeight = mTerrainHeight
        End Get
    End Property

    Public WriteOnly Property HeightMap() As Texture2D Implements ITerrain.HeightMap
        Set(ByVal value As Texture2D)
            mHeightMap = value
        End Set
    End Property

    Public ReadOnly Property HeightMapInfo() As HeightMapInfo Implements ITerrain.HeightMapInfo
        Get
            HeightMapInfo = mHeightMapInfo
        End Get
    End Property

    ''' <summary>
    ''' This list should filled before the terrain is initialized.
    ''' The terrain is divided into horizontal slices, corrisponding to the number 
    ''' of colors in this list. Terrain in each slice is colored accordingling.
    ''' </summary>
    Public ReadOnly Property Colors() As List(Of Color)
        Get
            Colors = mColors
        End Get
    End Property
#End Region ' Properties

#Region " Update & Draw "
    Public Overrides Sub Draw(ByVal elapsedSeconds As Single, ByVal camera As GameEngine.Camera3D) Implements ITerrain.Draw

        'GameManager.GraphicsDevice.RenderState.FillMode = FillMode.WireFrame
        'GameManager.GraphicsDevice.RenderState.CullMode = CullMode.None
        'GameManager.GraphicsDevice.Clear(ClearOptions.Target Or ClearOptions.DepthBuffer, Color.Black, 1.0F, 0)

        mEffect.CurrentTechnique = mEffect.Techniques("Colored")
        mEffect.Parameters("xView").SetValue(camera.View)
        mEffect.Parameters("xProjection").SetValue(camera.Projection)
        mEffect.Parameters("xWorld").SetValue(World)

        ' TODO - Even when having this enabled, directional lighting does not seem to be working
        mEffect.Parameters("xEnableLighting").SetValue(False)
        mEffect.Parameters("xLightDirection").SetValue(LightDirection)
        mEffect.Parameters("xAmbient").SetValue(AmbientLightingLevel)


        'mEffect.Begin()
        For Each pass As EffectPass In mEffect.CurrentTechnique.Passes
            pass.Apply()

            'GameManager.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, mVertices, 0, mVertices.Length, mIndices, 0, mIndices.Length / 3)

            'GameManager.GraphicsDevice.VertexDeclaration = mVertexDeclaration
            GameManager.GraphicsDevice.Indices = mIndexBuffer
            'GameManager.GraphicsDevice.Vertices(0).SetSource(mVertexBuffer, 0, VertexPositionNormalColor.SizeInBytes)
            GameManager.GraphicsDevice.SetVertexBuffer(mVertexBuffer)

            GameManager.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, mVertices.Length, 0, mIndices.Length / 3)
            'pass.End()
        Next
        'mEffect.End()

        MyBase.Draw(elapsedSeconds, camera)

    End Sub


#End Region ' Update & Draw

#Region " Private Methods "
    Private Sub CopyToBuffers()

        'mVertexBuffer = New VertexBuffer(GameManager.GraphicsDevice, mVertices.Length * VertexPositionNormalColor.SizeInBytes, BufferUsage.WriteOnly)
        'mVertexBuffer = New VertexBuffer(GameManager.GraphicsDevice, VertexPositionNormalColor.VertexDeclaration, mVertices.Length, BufferUsage.WriteOnly)
        mVertexBuffer = New VertexBuffer(GameManager.GraphicsDevice, GetType(VertexPositionNormalColor), mVertices.Length, BufferUsage.WriteOnly)

        mVertexBuffer.SetData(mVertices)

        mIndexBuffer = New IndexBuffer(GameManager.GraphicsDevice, GetType(Integer), mIndices.Length, BufferUsage.WriteOnly)
        mIndexBuffer.SetData(mIndices)
    End Sub

    ''' <remarks>Need to know the normals for lighting</remarks>
    Private Sub CalculateNormals()
        ' Clear normals
        For Each v As VertexPositionNormalColor In mVertices
            v.Normal = Vector3.Zero
        Next

        ' Calculate normals
        For i As Integer = 0 To mIndices.Length / 3 - 1
            Dim index1 As Integer = mIndices(i * 3)
            Dim index2 As Integer = mIndices(i * 3 + 1)
            Dim index3 As Integer = mIndices(i * 3 + 2)

            Dim side1 As Vector3 = mVertices(index1).Position - mVertices(index3).Position
            Dim side2 As Vector3 = mVertices(index1).Position - mVertices(index2).Position
            Dim normal As Vector3 = Vector3.Cross(side1, side2)

            mVertices(index1).Normal += normal
            mVertices(index2).Normal += normal
            mVertices(index3).Normal += normal
        Next

        ' Normalize vertices
        For i As Integer = 0 To mVertices.Length - 1
            mVertices(i).Normal.Normalize()
        Next
    End Sub

    Private Sub SetUpVertices()
        ' Find the highest and lowest points in the terrain
        Dim minHeight As Single = Single.MaxValue
        Dim maxHeight As Single = Single.MinValue
        For x As Integer = 0 To TerrainWidth - 1
            For y As Integer = 0 To TerrainHeight - 1
                If mHeightData(x, y) < minHeight Then
                    minHeight = mHeightData(x, y)
                End If
                If mHeightData(x, y) > maxHeight Then
                    maxHeight = mHeightData(x, y)
                End If
            Next
        Next

        ' Create array to hold vertices
        mVertices = New VertexPositionNormalColor(TerrainWidth * TerrainHeight - 1) {}

        ' Create vertices
        For x As Integer = 0 To TerrainWidth - 1
            For y As Integer = 0 To TerrainHeight - 1
                ' Set position
                mVertices(x + y * TerrainWidth).Position = New Vector3(x, mHeightData(x, y), -y)
                ' Set color
                mVertices(x + y * TerrainWidth).Color = SelectVertexColor(mHeightData(x, y), minHeight, maxHeight)
            Next
        Next

        mVertexDeclaration = VertexPositionColor.VertexDeclaration
    End Sub

    ''' <summary>
    ''' Terrain is divided into horizontal slices, corrisponding to the number of colors
    ''' in list mColors.
    ''' </summary>
    ''' <param name="height">Height to pick a color for.</param>
    ''' <param name="minHeight">Min height on the heightmap.</param>
    ''' <param name="maxHeight">Max height on the heightmap.</param>
    ''' <returns>Color appropriate to the height.</returns>
    Private Function SelectVertexColor(ByVal height As Single, ByVal minHeight As Single, ByVal maxHeight As Single) As Color
        For i As Integer = 1 To mColors.Count
            If height < minHeight + (maxHeight - minHeight) * i / mColors.Count Then
                Return mColors(i - 1)
            End If
        Next
        '' Else just return white
        SelectVertexColor = Color.Beige
    End Function

    Private Sub SetUpIndices()

        mIndices = New Integer((TerrainWidth - 1) * (TerrainHeight - 1) * 6 - 1) {}
        Dim counter As Integer = 0
        For y As Integer = 0 To TerrainHeight - 2
            For x As Integer = 0 To TerrainWidth - 2
                Dim lowerLeft As Integer = x + y * TerrainWidth
                Dim lowerRight As Integer = (x + 1) + y * TerrainWidth
                Dim topLeft As Integer = x + (y + 1) * TerrainWidth
                Dim topRight As Integer = (x + 1) + (y + 1) * TerrainWidth

                mIndices(counter) = topLeft
                counter += 1
                mIndices(counter) = lowerRight
                counter += 1
                mIndices(counter) = lowerLeft

                counter += 1
                mIndices(counter) = topLeft
                counter += 1
                mIndices(counter) = topRight
                counter += 1
                mIndices(counter) = lowerRight
                counter += 1
            Next
        Next
    End Sub

    Private Sub LoadHeightData(ByVal heightMap As Texture2D)
        mTerrainWidth = heightMap.Width
        mTerrainHeight = heightMap.Height

        Dim heightMapColors As Color() = New Color(TerrainWidth * TerrainHeight - 1) {}
        heightMap.GetData(heightMapColors)

        mHeightData = New Single(TerrainWidth - 1, TerrainHeight - 1) {}
        For x As Integer = 0 To TerrainWidth - 1
            For y As Integer = 0 To TerrainHeight - 1
                mHeightData(x, y) = heightMapColors(x + y * TerrainWidth).R / mTerrainBumpiness
            Next
        Next
    End Sub

#End Region ' Private Methods

    Public Shadows ReadOnly Property BoundingSphere() As Microsoft.Xna.Framework.BoundingSphere Implements ITerrain.BoundingSphere
        Get
            BoundingSphere = New BoundingSphere(Position, MathHelper.Max(TerrainWidth, TerrainHeight))
        End Get
    End Property

End Class